﻿using System;
using System.Collections.Generic;

namespace Asteroids.Entities
{
    /**
     * Add 
     *  SetEntityGroup
     *  RemoveEntityGroup
     *  
     *  SetEntityProcessorGroup
     *  RemoveEntityProcessorGroup
     *  
     * 
     */
    /// <summary>
    /// Manages <see cref="Xenon.Game.Entities.Entity"/> objects and their <see cref="Xenon.Game.Entities.EntityProcessor"/>s
    /// </summary>
    public class EntityManager
    {
        #region Fields

        private const string _globalGroup = "GLOBAL";

        private int _id = 0;
        private int _deleted = 0;
        private int _activeCount = 0;

        private IDictionary<int, EntityData> _entityDataMap;
        private IDictionary<Type, EntityProcessorData> _entityProcessorMap;

        private IDictionary<string, EntityGroup> _entityGroups;
        private IDictionary<string, EntityProcessorGroup> _processorGroups;

        #endregion

        /// <summary>
        /// 
        /// </summary>
        public EntityManager()
        {
            _entityDataMap = new Dictionary<int, EntityData>();
            _entityGroups = new Dictionary<string, EntityGroup>();
            _processorGroups = new Dictionary<string, EntityProcessorGroup>();
            _entityProcessorMap = new Dictionary<Type, EntityProcessorData>();

            _entityGroups.Add(_globalGroup, new EntityGroup(_globalGroup, this));
            _processorGroups.Add(_globalGroup, new EntityProcessorGroup(_globalGroup));
        }

        #region EntityProcessing

        /// <summary>
        /// Processes entities 
        /// </summary>
        /// <param name="processorGroup">The particular processor group to execute. Null executes all processors.</param>
        public void ProcessEntities(float delta, string processorGroupName = null, string entityGroupName = null)
        {
            // we run all of the processes
            if (string.IsNullOrWhiteSpace(processorGroupName))
                processorGroupName = _globalGroup;

            if (string.IsNullOrWhiteSpace(entityGroupName))
                entityGroupName = _globalGroup;

            if(_processorGroups.ContainsKey(processorGroupName) && _entityGroups.ContainsKey(entityGroupName))
            {
                // for each processor, process all of the entities inside of the specified entity group
                EntityProcessorGroup processorGroup = _processorGroups[processorGroupName];
                EntityGroup entityGroup = _entityGroups[entityGroupName];

                for (int index = 0; index < processorGroup.Count; index++)
                {
                    EntityProcessor processor = processorGroup[index];

                    processor.Begin(this);

                    processor.Update(delta);

                    if (!processor.ShouldSleep)
                    {
                        for (int entityIndex = 0; entityIndex < entityGroup.Count; entityIndex++)
                        {
                            Entity e = entityGroup[entityIndex];

                            if (e.Deleted)
                                DeleteEntity(e);
                            else if (e.Enabled)
                                processor.Process(e);
                        }
                    }

                    processor.End();
                }
            }
        }

        /// <summary>
        /// Adds an <see cref="Xenon.Game.Entities.EntityProcessor"/> to this manager
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="process"></param>
        public void AddProcessor<T>(T process, string group = null) where T : EntityProcessor
        {

            Type type = typeof(T);

            if (_entityProcessorMap.ContainsKey(type))
                throw new InvalidOperationException("This EntityProcessor has already been added.");
            else
            {
                _entityProcessorMap.Add(type, new EntityProcessorData(process));

                SetProcessorToGroup(process, _globalGroup);

                if (!string.IsNullOrWhiteSpace(group))
                    SetProcessorToGroup(process, group);

            }
        }

        /// <summary>
        /// Deletes an <see cref="Xenon.Game.Entities.EntityProcessor"/>  from this manager
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="process"></param>
        public void DeleteProcessor<T>(T process) where T : EntityProcessor
        {
            Type type = typeof(T);

            if(_entityProcessorMap.ContainsKey(type))
            {
                var entityMap = _entityProcessorMap[type];
                entityMap.RemoveFromAllGroups();
                _entityProcessorMap.Remove(type);
            }
            else
                throw new InvalidOperationException("This EntityProcessor could not be found.");
        }

        /// <summary>
        /// Adds an EntityProcessor to a group
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="processor"></param>
        /// <param name="groupName"></param>
        public void SetProcessorToGroup<T>(T processor, string groupName) where T : EntityProcessor
        {
            Type type = typeof(T);

            if (_entityProcessorMap.ContainsKey(type) && !string.IsNullOrWhiteSpace(groupName))
            {
                var processorData = _entityProcessorMap[type];
                EntityProcessorGroup processorGroup = null;

                if (_processorGroups.ContainsKey(groupName))
                    processorGroup = _processorGroups[groupName];
                else
                {
                    processorGroup = new EntityProcessorGroup(groupName);
                    _processorGroups.Add(groupName, processorGroup);
                }

                processorData.AddProcessorToGroup(processorGroup);
            }
            else
                throw new InvalidOperationException("Either this EntityProcessor is not within this EntityManager, or you are trying to modify the Global processor group");

        }

        /// <summary>
        /// Removes an EntityProcessor from a group
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="processor"></param>
        /// <param name="groupName"></param>
        public void RemoveProcessorFromGroup<T>(T processor, string groupName) where T : EntityProcessor
        {
            Type type = typeof(T);

            if (_entityProcessorMap.ContainsKey(type) || string.IsNullOrWhiteSpace(groupName))
                throw new InvalidOperationException();
            else
            {
                var processorData = _entityProcessorMap[type];
                processorData.RemoveFromAllGroups();
            }
        }

        #endregion

        #region EntityGrouping

        /// <summary>
        /// Sets an entity to an entity group
        /// </summary>
        /// <param name="id"></param>
        /// <param name="groupName"></param>
        public void SetEntityToGroup(int id, string groupName)
        {
            if (string.IsNullOrWhiteSpace(groupName))
                throw new ArgumentNullException("groupName", "Cannot modify the default entity group");

            // if the matching group exists, add the entity to it
            if (_entityDataMap.ContainsKey(id))
            {
                EntityGroup entityGroup = null;
                if (_entityGroups.ContainsKey(groupName))
                    entityGroup = _entityGroups[groupName];
                else
                {
                    entityGroup = new EntityGroup(groupName, this);
                    _entityGroups.Add(groupName, entityGroup);
                }

                _entityDataMap[id].AddEntityToGroup(entityGroup);
            }
            else
                throw new InvalidOperationException("The entity doesn't exist or the group in question could not be found");
        }

        /// <summary>
        /// Sets and entity to an entity group
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="groupName"></param>
        public void SetEntityToGroup(Entity entity, string groupName)
        { SetEntityToGroup(entity.ID, groupName); }

        /// <summary>
        /// Removes an entity from an entity group
        /// </summary>
        /// <param name="id"></param>
        /// <param name="groupName"></param>
        public void RemoveEntityFromGroup(int id, string groupName)
        {
            if (string.IsNullOrWhiteSpace(groupName))
                throw new ArgumentNullException("groupName", "Cannot modify the default entity group");

            if (_entityDataMap.ContainsKey(id) && _entityGroups.ContainsKey(groupName))
                _entityDataMap[id].RemoveEntityFromGroup(_entityGroups[groupName]);
            else
                throw new InvalidOperationException("The entity doesn't exist or the group in question could not be found");
        }

        /// <summary>
        /// Removes an entity from an entity group
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="groupName"></param>
        public void RemoveEntityFromGroup(Entity entity, string groupName)
        { RemoveEntityFromGroup(entity.ID, groupName); }
        
        #endregion

        #region EntityLifeCycle

        /// <summary>
        /// Creates a new instance of <see cref="Xenon.Game.Entities.Entity"/>
        /// </summary>
        /// <param name="group"></param>
        /// <returns></returns>
        public Entity CreateEntity(string group = null)
        { return CreateEntity(group, EntityTemplate.Default); }

        /// <summary>
        /// Creates and returns a new instance of an <see cref="Xenon.Game.Entities.Entity"/> after populating it with an <see cref="Xenon.Game.Entities.EntityTemplate"/>
        /// </summary>
        /// <param name="group"></param>
        /// <param name="description"></param>
        /// <returns></returns>
        public Entity CreateEntity(string group, EntityTemplate description)
        {
            Entity entity = new Entity(_id++, this);
            description.PopulateEntity(entity);
            _entityDataMap.Add(entity.ID, new EntityData(entity));

            entity.ActiveStateModified += new Action<Entity>(entity_ActiveStateModified);
            entity.Enabled = true;

            SetEntityToGroup(entity, _globalGroup);

            if (!string.IsNullOrWhiteSpace(group))
                SetEntityToGroup(entity, group);
            
            return entity;
        }

        /// <summary>
        /// Deletes an <see cref="Xenon.Game.Entities.Entity"/> from this manager
        /// </summary>
        /// <param name="e"></param>
        public void DeleteEntity(Entity e)
        {
            if (_entityDataMap.ContainsKey(e.ID))
            {
                e.Delete();

                var dataMap = _entityDataMap[e.ID];

                var groups = dataMap.EntityGroups;

                // remove from ALL entity groups that it may be a part of 
                dataMap.RemoveEntityFromAllGroups();

                _entityDataMap.Remove(e.ID);

                if (e.Enabled)
                    e.Enabled = false;

                _deleted++;
            }
            else
                throw new InvalidOperationException("This entity could not be found in this EntityManager.");
        }

        private void entity_ActiveStateModified(Entity obj)
        {
            if (obj.Enabled)
                _activeCount++;
            else
                _activeCount--;
        }

        #endregion

        /// <summary>
        /// Returns a read only collection containing all of the <see cref="Xenon.Game.Entities.EntityGroup"/>s set in this manager
        /// </summary>
        public ReadOnlyList<EntityGroup> Groups
        { get { return new ReadOnlyList<EntityGroup>(_entityGroups.Values); } }

        public int DeletedEntities
        { get { return _deleted; } }

        public int CreatedEntities
        { get { return _id; } }

        public int ActiveEntityCount
        { get { return _activeCount; } }
    }

    public struct EntityProcessorData
    {
        private EntityProcessor _processor;
        private IList<EntityProcessorGroup> _groups;

        public EntityProcessorData(EntityProcessor processor)
        {
            _processor = processor;
            _groups = new List<EntityProcessorGroup>();
        }

        public void AddProcessorToGroup(EntityProcessorGroup group)
        {
            if (_groups.Contains(group))
                throw new InvalidOperationException("This entity is already added to this group");
            else
            {
                group.Add(_processor);
                _groups.Add(group);
            }
        }

        public void RemoveProcessorFromGroup(EntityProcessorGroup group)
        {
            if (_groups.Contains(group))
            {
                _groups.Remove(group);
                group.Remove(_processor);
            }
            else
                throw new InvalidOperationException("This entity is not within the specified group");

        }

        public void RemoveFromAllGroups()
        {
            for (int index = 0; index < _groups.Count; index++)
            {
                var group = _groups[index];

                RemoveProcessorFromGroup(group);
            }
        }

        public EntityProcessor EntityProcessor
        { get { return _processor; } }

        public ReadOnlyList<EntityProcessorGroup> ProcessorGroups
        { get { return new ReadOnlyList<EntityProcessorGroup>(_groups); } }
    }

    public struct EntityData
    {
        private Entity _entity;
        private IList<EntityGroup> _groups;

        public EntityData(Entity entity)
        {
            _entity = entity;
            _groups = new List<EntityGroup>();
        }

        public void AddEntityToGroup(EntityGroup group)
        {
            if (group.Contains(_entity))
                throw new InvalidOperationException("This entity is already added to this group");

            group.AddEntity(_entity);
            _groups.Add(group);
        }

        public void RemoveEntityFromGroup(EntityGroup group)
        {
            if (group.Contains(_entity))
            {
                _groups.Remove(group);
                group.RemoveEntity(_entity);
            }
            else
                throw new InvalidOperationException("This entity is not within the specified group");
        }

        public void RemoveEntityFromAllGroups()
        {
            for (int index = 0; index < _groups.Count; index++)
            {
                var group = _groups[index];
                RemoveEntityFromGroup(group);
            }
        }

        public Entity Entity
        { get { return _entity; } }

        public ReadOnlyList<EntityGroup> EntityGroups
        { get { return new ReadOnlyList<EntityGroup>(_groups); } }

    
    }
}
